/**
 * www.easyplatform.cn ©2016
 */
package cn.easyplatform.studio.web.editors.support;

import nu.xom.*;
import nu.xom.Element;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.zkoss.lang.Objects;
import org.zkoss.util.resource.Locators;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;

import javax.xml.XMLConstants;
import java.io.InputStream;
import java.util.*;

import static org.zkoss.lang.Generics.cast;

/**
 * @author <a href="mailto:shiny_vc@163.com">陈云亮</a> <br/>
 * @since 2.0.0 <br/>
 */
public abstract class ZulXsdUtil {
	private static final ElementComparator ELEMENT_COMPARATOR = new ElementComparator();
	private static final SortedMap<String, SortedSet<Element>> emptyMap = new TreeMap<String, SortedSet<Element>>();
	private static final SortedSet<String> emptySet = new TreeSet<String>();
	private static final Document xsdDocument = new XsdDocumentBuilder()
			.build();
	private static Map<Class<? extends Component>, Component> defaults = cast(Collections
			.synchronizedMap(new HashMap<Class<? extends Component>, Component>()));
	// private static final String ZUL_NS = "http://www.zkoss.org/2005/zul";

	private static final XPathContext XPATH_CONTEXT_XS = new XPathContext("xs",
			XMLConstants.W3C_XML_SCHEMA_NS_URI);

	private static final Map<String, SortedSet<String>> widgetChildren = new HashMap<String, SortedSet<String>>();

	static {
		widgetChildren.put("scrollview", getWidgetChildren("scrollview"));
		widgetChildren.put("window", getWidgetChildren("window"));
		widgetChildren.put("panel", getWidgetChildren("panel"));
		widgetChildren.put("panelchildren", getWidgetChildren("panelchildren"));
		widgetChildren.put("tabbox", getWidgetChildren("tabbox"));
		widgetChildren.put("tab", getWidgetChildren("tab"));
		widgetChildren.put("tabpanel", getWidgetChildren("tabpanel"));
		widgetChildren.put("caption", getWidgetChildren("caption"));
		widgetChildren.put("groupbox", getWidgetChildren("groupbox"));
		widgetChildren.put("span", getWidgetChildren("span"));
		widgetChildren.put("div", getWidgetChildren("div"));
		widgetChildren.put("idspace", getWidgetChildren("idspace"));
		widgetChildren.put("grid", getWidgetChildren("grid"));
		widgetChildren.put("columns", getWidgetChildren("columns"));
		widgetChildren.put("column", getWidgetChildren("column"));
		widgetChildren.put("rows", getWidgetChildren("rows"));
		widgetChildren.put("row", getWidgetChildren("row"));
		widgetChildren.put("cell", getWidgetChildren("cell"));
		widgetChildren.put("cardlayout", getWidgetChildren("cardlayout"));
		widgetChildren.put("borderlayout", getWidgetChildren("borderlayout"));
		widgetChildren.put("north", getWidgetChildren("north"));
		widgetChildren.put("south", getWidgetChildren("south"));
		widgetChildren.put("east", getWidgetChildren("east"));
		widgetChildren.put("west", getWidgetChildren("west"));
		widgetChildren.put("center", getWidgetChildren("center"));
		widgetChildren.put("hlayout", getWidgetChildren("hlayout"));
		widgetChildren.put("vlayout", getWidgetChildren("vlayout"));
		widgetChildren.put("hbox", getWidgetChildren("hbox"));
		widgetChildren.put("vbox", getWidgetChildren("vbox"));
		widgetChildren.put("splitter", getWidgetChildren("splitter"));
		widgetChildren.put("tablelayout", getWidgetChildren("tablelayout"));
		widgetChildren.put("tablechildren", getWidgetChildren("tablechildren"));
		widgetChildren.put("columnlayout", getWidgetChildren("columnlayout"));
		widgetChildren.put("auxhead", getWidgetChildren("auxhead"));
		widgetChildren.put("listhead", getWidgetChildren("listhead"));
		widgetChildren.put("treecols", getWidgetChildren("treecols"));
		widgetChildren.put("listbox", getWidgetChildren("listbox"));
		widgetChildren.put("tree", getWidgetChildren("tree"));
		widgetChildren.put("orgchart", getWidgetChildren("orgchart"));
		widgetChildren.put("columnchildren",
				getWidgetChildren("columnchildren"));
		widgetChildren.put("portallayout", getWidgetChildren("portallayout"));
		widgetChildren.put("portalchildren",
				getWidgetChildren("portalchildren"));
		widgetChildren.put("absolutelayout",
				getWidgetChildren("absolutelayout"));
		widgetChildren.put("absolutechildren",
				getWidgetChildren("absolutechildren"));
		widgetChildren.put("anchorlayout", getWidgetChildren("anchorlayout"));
		widgetChildren.put("anchorchildren",
				getWidgetChildren("anchorchildren"));
		//widgetChildren.put("echarts", getWidgetChildren("echarts"));
	}

	public static SortedSet<String> getWidgetChildren(String widget) {
		SortedSet<String> result = widgetChildren.get(widget);
		if (result != null)
			return result;

		result = new TreeSet<String>();
		Element root = xsdDocument.getRootElement();

		Nodes nodes = root.query("xs:element[@name='" + widget + "']",
				XPATH_CONTEXT_XS);
		if (nodes.size() != 1)
			return emptySet;
		Element element = (Element) nodes.get(0);

		nodes = root.query(
				"xs:complexType[@name='" + element.getAttributeValue("type")
						+ "']//xs:element", XPATH_CONTEXT_XS);
		for (int i = 0; i < nodes.size(); i++) {
			result.add(((Element) nodes.get(i)).getAttributeValue("ref"));
		}

		nodes = root.query(
				"xs:complexType[@name='" + element.getAttributeValue("type")
						+ "']//xs:group", XPATH_CONTEXT_XS);
		for (int i = 0; i < nodes.size(); i++) {
			getWidgetChildren(
					((Element) nodes.get(i)).getAttributeValue("ref"), result);
		}

		return result;
	}

	public static boolean isAblechild(String container, String child) {
		SortedSet<String> cs = widgetChildren.get(container);
		if (cs != null) {
			Object[] result = cs.toArray();
			for (Object c : result) {
				if (c.equals(child))
					return true;
			}
		}
		return false;
	}

	public static boolean isContainer(String widget) {
		return widgetChildren.containsKey(widget);
	}

	private static void getWidgetChildren(String group, SortedSet<String> result) {
		Element root = xsdDocument.getRootElement();

		Nodes nodes = root.query("xs:group[@name='" + group + "']//xs:element",
				XPATH_CONTEXT_XS);
		for (int i = 0; i < nodes.size(); i++) {
			result.add(((Element) nodes.get(i)).getAttributeValue("ref"));
		}

		nodes = root.query("xs:group[@name='" + group + "']//xs:group",
				XPATH_CONTEXT_XS);
		for (int i = 0; i < nodes.size(); i++) {
			getWidgetChildren(
					((Element) nodes.get(i)).getAttributeValue("ref"), result);
		}
	}

	public static SortedMap<String, SortedSet<Element>> getWidgetDescription(
			String widget) {
		SortedMap<String, SortedSet<Element>> result = new TreeMap<String, SortedSet<Element>>();
		Element root = xsdDocument.getRootElement();

		Nodes nodes = root.query("xs:element[@name='" + widget + "']",
				XPATH_CONTEXT_XS);
		if (nodes.size() != 1)
			return emptyMap;
		Element element = (Element) nodes.get(0);

		nodes = root.query(
				"xs:complexType[@name='" + element.getAttributeValue("type")
						+ "']", XPATH_CONTEXT_XS);
		if (nodes.size() != 1)
			return emptyMap;
		Element type = (Element) nodes.get(0);

		for (int i = 0; i < type.getChildElements().size(); i++) {
			Element attr = type.getChildElements().get(i);
			if (attr.getLocalName().equals("attribute")) {

				if (!result.containsKey(widget)) {
					result.put(widget, new TreeSet<Element>(ELEMENT_COMPARATOR));
				}
				result.get(widget).add(attr);

			} else if (attr.getLocalName().equals("attributeGroup")) {
				populateAttributeGroups(attr.getAttributeValue("ref"), result);
			}

		}

		return result;
	}

	private static void populateAttributeGroups(String groupName,
			Map<String, SortedSet<Element>> result) {
		Element root = xsdDocument.getRootElement();

		Nodes nodes = root.query(
				"xs:attributeGroup[@name='" + groupName + "']",
				XPATH_CONTEXT_XS);
		if (nodes.size() != 1)
			return;
		Element attrGroup = (Element) nodes.get(0);

		for (int i = 0; i < attrGroup.getChildElements().size(); i++) {
			Element attr = attrGroup.getChildElements().get(i);
			if (attr.getLocalName().equals("attribute")) {

				// if (isBannedProperty(attr.getAttributeValue("name")))
				// continue;

				if (!result.containsKey(groupName)) {
					result.put(groupName, new TreeSet<Element>(
							ELEMENT_COMPARATOR));
				}
				result.get(groupName).add(attr);

			} else if (attr.getLocalName().equals("attributeGroup")) {
				populateAttributeGroups(attr.getAttributeValue("ref"), result);
			}
		}
	}

	public static List<String> getConstraintForAttributeType(
			String attributeType) {
		Element root = xsdDocument.getRootElement();

		Nodes nodes = root.query("xs:simpleType[@name='" + attributeType
				+ "']//xs:restriction/xs:enumeration", XPATH_CONTEXT_XS);
		if (nodes.size() == 0)
			return Collections.emptyList();

		List<String> restrictions = new ArrayList<String>();
		for (int i = 0; i < nodes.size(); i++) {
			restrictions.add(((Element) nodes.get(i))
					.getAttributeValue("value"));
		}

		return restrictions;
	}

	public static String getTypeOfAttribute(String element, String propertyName) {
		SortedMap<String, SortedSet<Element>> propsMap = getWidgetDescription(element);
		for (SortedSet<Element> group : propsMap.values()) {
			for (Element property : group) {
				if (property.getAttributeValue("name").equals(propertyName)) {
					return property.getAttributeValue("type");
				}
			}
		}

		return null;
	}

	public static boolean isBaseGroupElement(Element element) {
		Element root = xsdDocument.getRootElement();
		Nodes nodes = root.query(
				"xs:group[@name='baseGroup']//xs:element[@ref='"
						+ element.getLocalName() + "']", XPATH_CONTEXT_XS);
		return nodes.size() == 1;
	}

	public static String getXPath(Element element) {
		if (element == null)
			return null;
		Node parent = element.getParent();
		if (parent == null || !(parent instanceof Element)) {
			return "/" + element.getQualifiedName();
		}
		return getXPath((Element) parent) + "/" + element.getQualifiedName()
				+ "[" + getOccurenceOfChild((Element) parent, element) + "]";
	}

	public static Document buildDocument(String content) {
		try {
			return new Builder(false, new NodeFactory()).build(content, null);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Document buildDocument(InputStream content) {
		try {
			return new Builder(false, new NodeFactory()).build(content, null);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static void cleanUUIDs(Element parent) {
		Nodes nodes = parent.query("descendant-or-self::*[@uuid]",
				XPathContext.makeNamespaceContext(parent));
		for (int i = 0; i < nodes.size(); i++) {
			Attribute uuid = ((Element) nodes.get(i)).getAttribute("uuid");
			((Element) nodes.get(i)).removeAttribute(uuid);
		}
	}

	public static void traverseChildren(Component parent,
			Map<String, Object> params, ChildDelegate<Component> childDelegate) {
		if (params == null)
			params = new HashMap<String, Object>();
		childDelegate.onChild(parent, params);
		for (Component child : parent.getChildren()) {
			traverseChildren(child, new HashMap<String, Object>(params),
					childDelegate);
		}
	}

	public static void traverseChildren(Element parent,
			Map<String, Object> params, ChildDelegate<Element> childDelegate) {
		if (params == null)
			params = new HashMap<String, Object>();
		childDelegate.onChild(parent, params);
		for (int i = 0; i < parent.getChildElements().size(); i++) {
			traverseChildren(parent.getChildElements().get(i),
					new HashMap<String, Object>(params), childDelegate);
		}
	}

	public static boolean isNative(Element element) {
		if (element == null)
			return false;
		String ns = element.getNamespaceURI();
		return "native".equals(ns)
				|| LanguageDefinition.NATIVE_NAMESPACE.equals(ns)
				|| element.getQualifiedName().startsWith(
						LanguageDefinition.NATIVE_NAMESPACE_PREFIX);
	}

	public static boolean isCodeElement(Element element) {
		String tagname = element.getLocalName();
		return "attribute".equals(tagname) || "script".equals(tagname)
				|| "zscript".equals(tagname) || "style".equals(tagname)
				|| "html".equals(tagname);
	}

	public static String getClientNamespacePrefix(
			cn.easyplatform.studio.web.editors.support.Element element) {
		Map<String, String> ns = element.getNamespacePrefixInScope();
		for (String prefix : ns.keySet()) {
			String uri = ns.get(prefix);
			if ("client".equals(uri)
					|| LanguageDefinition.CLIENT_NAMESPACE.equals(uri)) {
				return prefix;
			}
		}
		return null;
	}

	public static String getClientNamespace(
			cn.easyplatform.studio.web.editors.support.Element element) {
		Map<String, String> ns = element.getNamespacePrefixInScope();
		return ns.get(getClientNamespacePrefix(element));
	}

	public static boolean isDefaultValueForProperty(Component instance,
			String propertyName, Object value, boolean isBoolen) {
		if ("instant".equals(propertyName) || "focus".equals(propertyName))
			return Boolean.FALSE == value;
		if (!defaults.containsKey(instance.getClass()))
			defaults.put(instance.getClass(), instance.getDefinition()
					.newInstance(null, null));
		try {
			Object defvalue = MethodUtils.invokeMethod(
					defaults.get(instance.getClass()),
					(isBoolen ? "is" : "get")
							+ StringUtils.capitalize(propertyName));
			if (defvalue != null)
				return Objects.equals(value, defvalue);
			return false;
		} catch (Exception e) {
		}
		return false;
	}

	public static Object getDefaultValueForProperty(Component instance,
			String propertyName, boolean isBoolen) {
		if ("instant".equals(propertyName) || "focus".equals(propertyName))
			return Boolean.FALSE;
		if (!defaults.containsKey(instance.getClass()))
			defaults.put(instance.getClass(), instance.getDefinition()
					.newInstance(null, null));
		try {
			return MethodUtils.invokeMethod(
					defaults.get(instance.getClass()),
					(isBoolen ? "is" : "get")
							+ StringUtils.capitalize(propertyName));
		} catch (Exception e) {
			return null;
		}
	}

	public static void invokeSetter(Object instance, String property,
			Object value) throws Exception {
		MethodUtils.invokeMethod(instance,
				"set" + StringUtils.capitalize(property), value);
	}

	private static int getOccurenceOfChild(Element parent, Element child) {
		int occurence = 0;
		for (int i = 0; i < parent.getChildElements().size(); i++) {
			if (parent.getChildElements().get(i).getQualifiedName()
					.equals(child.getQualifiedName())) {
				occurence++;
			}
			if (parent.getChildElements().get(i).equals(child)) {
				return occurence;
			}
		}
		return 0;
	}

	private static class XsdDocumentBuilder {

		Document build() {
			try {
				return new Builder(false).build(Locators.getDefault()
						.getResourceAsStream("web/support/zul.xsd"), null);
			} catch (Exception e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}
		}
	}

	private static class ElementComparator implements Comparator<Element> {

		@Override
		public int compare(Element o1, Element o2) {
			return o1.getAttributeValue("name").compareTo(
					o2.getAttributeValue("name"));
		}
	}
}
